import threading
import functools
import inspect
import Queue

def event(proc):
    arg_names, _, _ = inspect.getargs(proc.__code__)
    arg_names = arg_names[1:] # remove 'self'

    @functools.wraps(proc)
    def handler(self, *args, **kw):
        # pre conditions or logging
        proc(self, *args, **kw)

        # post event
        e = Event(proc.__name__, args, arg_names)
        self.post(e)
        return e

    return handler

class Event:
    def __init__(self, name, args, arg_names):
        self.name       = name
        self.args       = args

        for key, value in zip(arg_names, args):
            setattr(self, key, value)

        self._lock      = threading.Condition()
        self._finished  = False
        self._reply     = None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        with self._lock:
            self._finished = True
            self._lock.notifyAll()

    def reply(self, res):
        self._reply = res

    def waitReply(self):
        lock = self._lock
        with lock:
            while not self._finished:
                lock.wait()
        return self._reply

class Automaton:
    def __init__(self):
        self._events    = Queue.Queue()
        self._finished  = False
        self._start()

    @property
    def isFinished(self):
        return self._finished

    def post(self, e):
        self._events.put(e)

    def wait(self, *names):
        while True:
            e = self._events.get()

            if e.name in names:
                return e

            self.unhandled(e)

    def waitAny(self):
        return self._events.get()

    def unhandled(self, event):
        """ Called at unwanted event. """
        pass

    def initialState(self):
        raise StopIteration()
        
    def body(self):
        while True:
            try:
                state = self.state
            except AttributeError:
                self.state = state = self.initialState

            try:
                state()
            except StopIteration:
                break

    def join(self):
        self._thread.join()

    def _start(self):
        def run():
            try:
                self.body()
            finally:
                self._finished = True
                self._events   = None

        self._thread = t = threading.Thread(target=run)
        t.daemon = True
        t.start()

if __name__ == "__main__":
    def test1():
        class A(Automaton):
            @event
            def e1(self, message):
                print("send e1: " + message)

            def body(self):
                with self.wait("e1") as e:
                    print("recv e1: " + e.message)
                    e.reply("Good!")

        a = A()
        e = a.e1("How are you?")
        res = e.waitReply()
        print("res: " + res)

    def test2():
        class B(Automaton):
            @event
            def e1(self): pass

            @event
            def e2(self): pass

            @event
            def msg(self, message):
                print("sending 'msg': " + message)

            def initialState(self):
                with self.wait("e1", "e2") as e:
                    if e.name == "e1":
                        self.state = self.state_1
                    else:
                        self.state = self.state_2

            def state_1(self):
                with self.wait("msg") as e:
                    print("in state_1: " + e.message)
                self.state = self.state_stop

            def state_2(self):
                with self.wait("msg") as e:
                    print("in state_2: " + e.message)
                self.state = self.state_stop

            def state_stop(self):
                print("stopping")
                raise StopIteration

        b = B()
        b.e1()
        b.msg("test")
        b.join()

        b = B()
        b.e2()
        b.msg("test 2")
        b.join()

    test1()
    test2()